package nsfs

import (
	"GLibHac/fssystem"
	"encoding/hex"
	"fmt"
	"github.com/pkg/errors"
	"os"
	"strings"
)

const NczMagic = "NCZSECTN"
const NczOffset = 0x4000

type PartitionDirectory struct {
	parentFileSystem *PartitionFileSystem
	path             string
	mode             OpenDirectoryMode
	currentIndex     int
}

func (p *PartitionDirectory) Read(entryBuffer []*DirectoryEntry) (int, error) {
	if !p.mode.HasFlag(OpenDirectoryModeFile) {
		return 0, nil
	}
	entriesRemaining := len(p.parentFileSystem.Files) - p.currentIndex
	toRead := len(entryBuffer)
	if entriesRemaining < toRead {
		toRead = entriesRemaining
	}
	for i := 0; i < toRead; i++ {
		fileEntry := p.parentFileSystem.Files[p.currentIndex]
		entryBuffer[i] = &DirectoryEntry{
			Name: fileEntry.Name,
			Type: DirectoryEntryTypeFile,
			Size: fileEntry.Size,
		}
		p.currentIndex++
	}
	return toRead, nil
}

type PartitionFileEntry struct {
	Index              int32
	Offset             int64
	Size               int64
	StringTableOffset  int64
	HashedRegionOffset int64
	HashedRegionSize   int32
	Hash               string
	Name               string
}

func NewPartitionFileEntry(reader fssystem.IStorage, Type *PartitionFileSystemType, index int32) (*PartitionFileEntry, error) {
	entry := &PartitionFileEntry{
		Index: index,
	}
	var err error
	entry.Offset, err = reader.ReadInt64()
	if err != nil {
		return nil, err
	}
	entry.Size, err = reader.ReadInt64()
	if v, err := reader.ReadUint32(); err != nil {
		return nil, err
	} else {
		entry.StringTableOffset = int64(v)
	}
	if Type != nil && Type.Magic == PartitionFileSystemTypeHashed.Magic {
		entry.HashedRegionSize, err = reader.ReadInt()
		if err != nil {
			return nil, err
		}
		entry.HashedRegionOffset, err = reader.ReadInt64()
		if err != nil {
			return nil, err
		}
		if v, err := reader.ReadBytes(0x20); err != nil {
			return nil, err
		} else {
			entry.Hash = hex.EncodeToString(v)
		}
	} else {
		_, err = reader.ReadBytes(4)
	}
	return entry, err
}

type PartitionFileSystemHeader struct {
	Magic           string
	NumFiles        int32
	StringTableSize int32
	Reserved        int32
	Type            *PartitionFileSystemType
	HeaderSize      int32
	Files           []*PartitionFileEntry
}

func NewPartitionFileSystemHeader(reader fssystem.IStorage) (header *PartitionFileSystemHeader, err error) {
	header = &PartitionFileSystemHeader{}
	header.Magic, err = reader.ReadUtf8Z(4)
	header.NumFiles, err = reader.ReadInt()
	header.StringTableSize, err = reader.ReadInt()
	header.Reserved, err = reader.ReadInt()
	header.Type = ConvertToPartitionFileSystemType(header.Magic)
	if header.Type == nil {
		err = errors.New("Invalid Partition FS type \"" + header.Magic + "\"")
		return
	}
	stringTableOffset := 16 + header.Type.EntrySize*header.NumFiles
	header.HeaderSize = stringTableOffset + header.StringTableSize
	header.Files = make([]*PartitionFileEntry, header.NumFiles)
	for i := int32(0); i < header.NumFiles; i++ {
		header.Files[i], err = NewPartitionFileEntry(reader, header.Type, i)
		if err != nil {
			return nil, err
		}
	}
	for i := int32(0); i < header.NumFiles; i++ {
		err = reader.SetPosition(int64(stringTableOffset) + header.Files[i].StringTableOffset)
		if err != nil {
			return nil, err
		}
		header.Files[i].Name, err = reader.ReadAsciiZ()
		if err != nil {
			return nil, err
		}
	}
	return
}

type PartitionFileSystem struct {
	iFileSystemAbstract
	Header      *PartitionFileSystemHeader
	HeaderSize  int32
	Files       []*PartitionFileEntry
	FileDict    map[string]*PartitionFileEntry
	baseStorage fssystem.IStorage
}

func (p *PartitionFileSystem) OpenFile(filePath string, openFileMode OpenFileMode) (fssystem.IStorage, error) {
	if f, ok := p.FileDict[strings.TrimPrefix(filePath, "/")]; !ok {
		return nil, errors.WithStack(fmt.Errorf("failed to open file \"%s\": %w", filePath, os.ErrNotExist))
	} else {
		return p.OpenPartitionFileEntry(f)
	}
}

func (p *PartitionFileSystem) OpenPartitionFileEntry(entry *PartitionFileEntry) (fssystem.IStorage, error) {
	offset := int64(p.HeaderSize) + entry.Offset
	storage := p.baseStorage.SliceRange(offset, entry.Size)
	if is, err := IsNcz(storage); err != nil {
		return nil, err
	} else if is {
		return nil, errors.Errorf("NCZ partition file entry is not supported")
	}
	return storage, nil
}

func IsNcz(storage fssystem.IStorage) (bool, error) {
	if storage.Size() < NczOffset+8 {
		return false, nil
	}
	position := storage.Position()
	if err := storage.SetPosition(NczOffset); err != nil {
		return false, err
	}
	bytes, err := storage.ReadBytes(8)
	ret := false
	if err == nil {
		ret = string(bytes) == NczMagic
	}
	if err := storage.SetPosition(position); err != nil {
		return ret, err
	}
	return false, nil
}

func (p *PartitionFileSystem) OpenDirectory(path string, openDirectoryMode OpenDirectoryMode) (IDirectory, error) {
	return &PartitionDirectory{
		path:             strings.TrimPrefix(path, "/"),
		mode:             openDirectoryMode,
		parentFileSystem: p,
	}, nil
}

func NewPartitionFileSystem(storage fssystem.IStorage) (pfs *PartitionFileSystem, err error) {
	pfs = &PartitionFileSystem{
		baseStorage: storage,
	}
	pfs.openFileFunc = pfs.OpenFile
	pfs.openDirectory = pfs.OpenDirectory
	pfs.Header, err = NewPartitionFileSystemHeader(storage.AsStorage())
	if err != nil {
		return
	}
	pfs.Files = pfs.Header.Files
	pfs.HeaderSize = pfs.Header.HeaderSize
	pfs.FileDict = make(map[string]*PartitionFileEntry, len(pfs.Files))
	for _, fileEntry := range pfs.Files {
		pfs.FileDict[fileEntry.Name] = fileEntry
	}
	return
}
